 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.internal.variables;

 import java.io.ByteArrayInputStream ;
 import java.io.ByteArrayOutputStream ;
 import java.io.IOException ;
 import java.io.UnsupportedEncodingException ;
 import java.util.ArrayList ;
 import java.util.HashMap ;
 import java.util.List ;
 import java.util.Map ;

 import javax.xml.parsers.DocumentBuilder ;
 import javax.xml.parsers.DocumentBuilderFactory ;
 import javax.xml.parsers.FactoryConfigurationError ;
 import javax.xml.parsers.ParserConfigurationException ;
 import javax.xml.transform.OutputKeys ;
 import javax.xml.transform.Transformer ;
 import javax.xml.transform.TransformerException ;
 import javax.xml.transform.TransformerFactory ;
 import javax.xml.transform.dom.DOMSource ;
 import javax.xml.transform.stream.StreamResult ;

 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IConfigurationElement;
 import org.eclipse.core.runtime.IExtensionPoint;
 import org.eclipse.core.runtime.ISafeRunnable;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.ListenerList;
 import org.eclipse.core.runtime.MultiStatus;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.Preferences;
 import org.eclipse.core.runtime.SafeRunner;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.Preferences.IPropertyChangeListener;
 import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
 import org.eclipse.core.variables.IDynamicVariable;
 import org.eclipse.core.variables.IStringVariable;
 import org.eclipse.core.variables.IStringVariableManager;
 import org.eclipse.core.variables.IValueVariable;
 import org.eclipse.core.variables.IValueVariableListener;
 import org.eclipse.core.variables.VariablesPlugin;
 import org.eclipse.osgi.util.NLS;
 import org.w3c.dom.Document ;
 import org.w3c.dom.Element ;
 import org.w3c.dom.Node ;
 import org.w3c.dom.NodeList ;
 import org.xml.sax.SAXException ;
 import org.xml.sax.helpers.DefaultHandler ;

 /**
  * Singleton string variable manager.
  */
 public class StringVariableManager implements IStringVariableManager, IPropertyChangeListener {
     
     /**
      * Dynamic variables - maps variable names to variables.
      */
     private Map fDynamicVariables;
     
     /**
      * Value variables - maps variable names to variables.
      */
     private Map fValueVariables;
     
     /**
      * Variable listeners
      */
     private ListenerList fListeners;
     
     // notifications
 private static final int ADDED = 0;
     private static final int CHANGED = 1;
     private static final int REMOVED = 2;
     
     /**
      * Singleton variable manager.
      */
     private static StringVariableManager fgManager;
     
     // true during internal updates indicates that change notification
 // should be suppressed/ignored.
 private boolean fInternalChange = false;
     
     // Variable extension point constants
 private static final String ATTR_NAME= "name"; //$NON-NLS-1$
 private static final String ATTR_DESCRIPTION="description"; //$NON-NLS-1$
 private static final String ATTR_READ_ONLY="readOnly"; //$NON-NLS-1$
 // Persisted variable XML constants
 private static final String VALUE_VARIABLES_TAG= "valueVariables"; //$NON-NLS-1$
 private static final String VALUE_VARIABLE_TAG= "valueVariable"; //$NON-NLS-1$
 private static final String NAME_TAG= "name"; //$NON-NLS-1$
 private static final String VALUE_TAG= "value"; //$NON-NLS-1$
 private static final String DESCRIPTION_TAG="description"; //$NON-NLS-1$
 private static final String READ_ONLY_TAG="readOnly"; //$NON-NLS-1$
 // XML values
 private static final String TRUE_VALUE= "true"; //$NON-NLS-1$
 private static final String FALSE_VALUE= "false"; //$NON-NLS-1$
 // preference store key for value variables
 private static final String PREF_VALUE_VARIABLES= VariablesPlugin.getUniqueIdentifier() + ".valueVariables"; //$NON-NLS-1$

     /**
      * Notifies a string variable listener in a safe runnable to handle
      * exceptions.
      */
     class StringVariableNotifier implements ISafeRunnable {
         
         private IValueVariableListener fListener;
         private int fType;
         private IValueVariable[] fVariables;
         
         /**
          * @see org.eclipse.core.runtime.ISafeRunnable#handleException(java.lang.Throwable)
          */
         public void handleException(Throwable exception) {
             IStatus status = new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), VariablesPlugin.INTERNAL_ERROR, "An exception occurred during string variable change notification", exception); //$NON-NLS-1$
 VariablesPlugin.log(status);
         }

         /**
          * @see org.eclipse.core.runtime.ISafeRunnable#run()
          */
         public void run() throws Exception {
             switch (fType) {
                 case ADDED:
                     fListener.variablesAdded(fVariables);
                     break;
                 case REMOVED:
                     fListener.variablesRemoved(fVariables);
                     break;
                 case CHANGED:
                     fListener.variablesChanged(fVariables);
                     break;
             }
         }

         /**
          * Notifies the given listener of the add/change/remove
          *
          * @param listener the listener to notify
          * @param launch the launch that has changed
          * @param update the type of change
          */
         public void notify(IValueVariable[] variables, int update) {
             fVariables = variables;
             fType = update;
             Object [] copiedListeners= fListeners.getListeners();
             for (int i= 0; i < copiedListeners.length; i++) {
                 fListener = (IValueVariableListener)copiedListeners[i];
                 SafeRunner.run(this);
             }
             fVariables = null;
             fListener = null;
             // persist variables whenever there is an add/change/remove
 storeValueVariables();
         }
     }
     
     /**
      * Returns a new notifier.
      *
      * @return a new notifier
      */
     private StringVariableNotifier getNotifier() {
         return new StringVariableNotifier();
     }
     
     /**
      * Returns the default string variable manager
      *
      * @return string variable manager
      */
     public static StringVariableManager getDefault() {
         if (fgManager == null) {
             fgManager = new StringVariableManager();
         }
         return fgManager;
     }
     
     /**
      * Constructs a new string variable manager.
      */
     private StringVariableManager() {
         fListeners = new ListenerList();
     }

     /**
      * Load contributed variables and persisted variables
      */
     private synchronized void initialize() {
         if (fDynamicVariables == null) {
             fInternalChange = true;
             fDynamicVariables = new HashMap (5);
             fValueVariables = new HashMap (5);
             loadContributedValueVariables();
             loadPersistedValueVariables();
             loadDynamicVariables();
             VariablesPlugin.getDefault().getPluginPreferences().addPropertyChangeListener(this);
             fInternalChange = false;
         }
     }
     
     /**
      * Loads contributed dynamic variables
      */
     private void loadDynamicVariables() {
         IExtensionPoint point = Platform.getExtensionRegistry().getExtensionPoint(VariablesPlugin.PI_CORE_VARIABLES, EXTENSION_POINT_DYNAMIC_VARIABLES);
         IConfigurationElement elements[]= point.getConfigurationElements();
         for (int i = 0; i < elements.length; i++) {
             IConfigurationElement element = elements[i];
             String name= element.getAttribute(ATTR_NAME);
             if (name == null) {
                 VariablesPlugin.logMessage(NLS.bind("Variable extension missing required 'name' attribute: {0}", new String [] {element.getDeclaringExtension().getLabel()}), null); //$NON-NLS-1$
 continue;
             }
             String description= element.getAttribute(ATTR_DESCRIPTION);
             DynamicVariable variable= new DynamicVariable(name, description, element);
             fDynamicVariables.put(variable.getName(), variable);
         }
     }

     /**
      * Loads contributed value variables. This is done before loading persisted values.
      */
     private void loadContributedValueVariables() {
         IExtensionPoint point = Platform.getExtensionRegistry().getExtensionPoint(VariablesPlugin.PI_CORE_VARIABLES, EXTENSION_POINT_VALUE_VARIABLES);
         IConfigurationElement elements[]= point.getConfigurationElements();
         for (int i = 0; i < elements.length; i++) {
             IConfigurationElement element = elements[i];
             String name= element.getAttribute(ATTR_NAME);
             if (name == null) {
                 VariablesPlugin.logMessage(NLS.bind("Variable extension missing required 'name' attribute: {0}", new String [] {element.getDeclaringExtension().getLabel()}), null); //$NON-NLS-1$
 continue;
             }
             String description= element.getAttribute(ATTR_DESCRIPTION);
             boolean isReadOnly = TRUE_VALUE.equals(element.getAttribute(ATTR_READ_ONLY));
             
             IValueVariable variable = new ContributedValueVariable(name, description, isReadOnly, element);
             fValueVariables.put(name, variable);
         }
     }

     /**
      * Loads persisted value variables from the preference store. This is done after
      * loading value variables from the extension point. If a persisted variable has the
      * same name as a extension contributed variable the variable's value will be set to
      * the persisted value unless either a) The persisted value is <code>null</code>, or
      * b) the variable is read-only.
      */
     private void loadPersistedValueVariables() {
         String variablesString= VariablesPlugin.getDefault().getPluginPreferences().getString(PREF_VALUE_VARIABLES);
         if (variablesString.length() == 0) {
             return;
         }
         Element root= null;
         Throwable ex = null;
         try {
             ByteArrayInputStream stream = new ByteArrayInputStream (variablesString.getBytes("UTF-8")); //$NON-NLS-1$
 DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
             parser.setErrorHandler(new DefaultHandler ());
             root = parser.parse(stream).getDocumentElement();
         } catch (UnsupportedEncodingException e) {
             ex = e;
         } catch (ParserConfigurationException e) {
             ex = e;
         } catch (FactoryConfigurationError e) {
             ex = e;
         } catch (SAXException e) {
             ex = e;
         } catch (IOException e) {
             ex = e;
         }
         if (ex != null) {
             VariablesPlugin.logMessage("An exception occurred while loading persisted value variables.", ex); //$NON-NLS-1$
 return;
         }
         if (!root.getNodeName().equals(VALUE_VARIABLES_TAG)) {
             VariablesPlugin.logMessage("Invalid format encountered while loading persisted value variables.", null); //$NON-NLS-1$
 return;
         }
         NodeList list= root.getChildNodes();
         for (int i= 0, numItems= list.getLength(); i < numItems; i++) {
             Node node= list.item(i);
             if (node.getNodeType() == Node.ELEMENT_NODE) {
                 Element element= (Element) node;
                 if (!element.getNodeName().equals(VALUE_VARIABLE_TAG)) {
                     VariablesPlugin.logMessage(NLS.bind("Invalid XML element encountered while loading value variables: {0}", new String [] {node.getNodeName()}), null); //$NON-NLS-1$
 continue;
                 }
                 String name= element.getAttribute(NAME_TAG);
                 if (name.length() > 0) {
                     String value= element.getAttribute(VALUE_TAG);
                     String description= element.getAttribute(DESCRIPTION_TAG);
                     boolean readOnly= TRUE_VALUE.equals(element.getAttribute(READ_ONLY_TAG));
                 
                     IValueVariable existing = getValueVariable(name);
                     if (existing == null){
                         ValueVariable variable = new ValueVariable(name, description, readOnly, value);
                         fValueVariables.put(name, variable);
                     } else if (!existing.isReadOnly() && value != null){
                         existing.setValue(value);
                     }
                 } else {
                     VariablesPlugin.logMessage("Invalid variable entry encountered while loading value variables. Variable name is null.", null); //$NON-NLS-1$
 }
             }
         }
     }
     
     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#getVariables()
      */
     public synchronized IStringVariable[] getVariables() {
         initialize();
         List list = new ArrayList (fDynamicVariables.size() + fValueVariables.size());
         list.addAll(fDynamicVariables.values());
         list.addAll(fValueVariables.values());
         return (IStringVariable[]) list.toArray(new IStringVariable[list.size()]);
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#getValueVariables()
      */
     public synchronized IValueVariable[] getValueVariables() {
         initialize();
         return (IValueVariable[]) fValueVariables.values().toArray(new IValueVariable[fValueVariables.size()]);
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#getDynamicVariables()
      */
     public synchronized IDynamicVariable[] getDynamicVariables() {
         initialize();
         return (IDynamicVariable[]) fDynamicVariables.values().toArray(new IDynamicVariable[fDynamicVariables.size()]);
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#performStringSubstitution(java.lang.String)
      */
     public String performStringSubstitution(String expression) throws CoreException {
         return performStringSubstitution(expression, true);
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#newValueVariable(java.lang.String, java.lang.String)
      */
     public IValueVariable newValueVariable(String name, String description) {
         return newValueVariable(name, description, false, null);
     }
     
     /* (non-Javadoc)
      * @see org.eclipse.core.variables.IStringVariableManager#newValueVariable(java.lang.String, java.lang.String, boolean, java.lang.String)
      */
     public IValueVariable newValueVariable(String name, String description, boolean readOnly, String value) {
         return new ValueVariable(name, description, readOnly, value);
     }
     
     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#addVariables(org.eclipse.debug.internal.core.stringsubstitution.IValueVariable[])
      */
     public synchronized void addVariables(IValueVariable[] variables) throws CoreException {
         initialize();
         MultiStatus status = new MultiStatus(VariablesPlugin.getUniqueIdentifier(), VariablesPlugin.INTERNAL_ERROR, VariablesMessages.StringVariableManager_26, null);
         for (int i = 0; i < variables.length; i++) {
             IValueVariable variable = variables[i];
             if (getValueVariable(variable.getName()) != null) {
                 status.add(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), VariablesPlugin.INTERNAL_ERROR, NLS.bind(VariablesMessages.StringVariableManager_27, new String []{variable.getName()}), null));
             }
         }
         if (status.isOK()) {
             for (int i = 0; i < variables.length; i++) {
                 IValueVariable variable = variables[i];
                 fValueVariables.put(variable.getName(), variable);
             }
             IValueVariable[] copy = new IValueVariable[variables.length];
             System.arraycopy(variables, 0, copy, 0, variables.length);
             getNotifier().notify(copy, ADDED);
             return;
         }
         throw new CoreException(status);
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#removeVariables(org.eclipse.debug.internal.core.stringsubstitution.IValueVariable[])
      */
     public synchronized void removeVariables(IValueVariable[] variables) {
         initialize();
         List removed = new ArrayList (variables.length);
         for (int i = 0; i < variables.length; i++) {
             IValueVariable variable = variables[i];
             if (fValueVariables.remove(variable.getName()) != null) {
                 removed.add(variable);
             }
         }
         if (removed.size() > 0) {
             getNotifier().notify((IValueVariable[])removed.toArray(new IValueVariable[removed.size()]), REMOVED);
         }
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#getDynamicVariable(java.lang.String)
      */
     public synchronized IDynamicVariable getDynamicVariable(String name) {
         initialize();
         return (IDynamicVariable) fDynamicVariables.get(name);
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#getValueVariable(java.lang.String)
      */
     public synchronized IValueVariable getValueVariable(String name) {
         initialize();
         return (IValueVariable) fValueVariables.get(name);
     }


     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#addValueVariableListener(org.eclipse.debug.internal.core.stringsubstitution.IValueVariableListener)
      */
     public void addValueVariableListener(IValueVariableListener listener) {
         fListeners.add(listener);
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#removeValueVariableListener(org.eclipse.debug.internal.core.stringsubstitution.IValueVariableListener)
      */
     public void removeValueVariableListener(IValueVariableListener listener) {
         fListeners.remove(listener);
     }
     
     /**
      * Returns a memento representing the value variables currently registered.
      *
      * @return memento representing the value variables currently registered
      * @throws IOException if an I/O exception occurs while creating the XML.
      */
     private String getValueVariablesAsXML() throws IOException , ParserConfigurationException , TransformerException {
         IValueVariable[] variables = getValueVariables();

         Document document= getDocument();
         Element rootElement= document.createElement(VALUE_VARIABLES_TAG);
         document.appendChild(rootElement);
         for (int i = 0; i < variables.length; i++) {
             IValueVariable variable = variables[i];
             if (!variable.isReadOnly()){
                 // don't persist read-only variables or un-initialized contributed variables
 if (!variable.isContributed() || ((ContributedValueVariable)variable).isInitialized()) {
                     Element element= document.createElement(VALUE_VARIABLE_TAG);
                     element.setAttribute(NAME_TAG, variable.getName());
                     String value= variable.getValue();
                     if (value != null) {
                         element.setAttribute(VALUE_TAG, value);
                     }
                     element.setAttribute(READ_ONLY_TAG, variable.isReadOnly() ? TRUE_VALUE : FALSE_VALUE);
                     String description= variable.getDescription();
                     if (description != null) {
                         element.setAttribute(DESCRIPTION_TAG, description);
                     }
                     rootElement.appendChild(element);
                 }
             }
         }
         return serializeDocument(document);
     }
     
     private Document getDocument() throws ParserConfigurationException {
         DocumentBuilderFactory dfactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder docBuilder = dfactory.newDocumentBuilder();
         Document doc =docBuilder.newDocument();
         return doc;
     }
     
     /**
      * Serializes a XML document into a string - encoded in UTF8 format,
      * with platform line separators.
      *
      * @param doc document to serialize
      * @return the document as a string
      * @throws TransformerException if an unrecoverable error occurs during the serialization
      * @throws IOException if the encoding attempted to be used is not supported
      */
     private String serializeDocument(Document doc) throws TransformerException , UnsupportedEncodingException {
         ByteArrayOutputStream s= new ByteArrayOutputStream ();
         
         TransformerFactory factory= TransformerFactory.newInstance();
         Transformer transformer= factory.newTransformer();
         transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$

         DOMSource source= new DOMSource (doc);
         StreamResult outputTarget= new StreamResult (s);
         transformer.transform(source, outputTarget);
         
         return s.toString("UTF8"); //$NON-NLS-1$
 }
     
     /**
      * Saves the value variables currently registered in the
      * preference store.
      */
     private synchronized void storeValueVariables() {
         Preferences prefs= VariablesPlugin.getDefault().getPluginPreferences();
         String variableString= ""; //$NON-NLS-1$
 if (!fValueVariables.isEmpty()) {
             try {
                 variableString= getValueVariablesAsXML();
             } catch (IOException e) {
                 VariablesPlugin.log(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), IStatus.ERROR, "An exception occurred while storing launch configuration variables.", e)); //$NON-NLS-1$
 return;
             } catch (ParserConfigurationException e) {
                 VariablesPlugin.log(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), IStatus.ERROR, "An exception occurred while storing launch configuration variables.", e)); //$NON-NLS-1$
 return;
             } catch (TransformerException e) {
                 VariablesPlugin.log(new Status(IStatus.ERROR, VariablesPlugin.getUniqueIdentifier(), IStatus.ERROR, "An exception occurred while storing launch configuration variables.", e)); //$NON-NLS-1$
 return;
             }
         }
         fInternalChange = true;
         prefs.setValue(PREF_VALUE_VARIABLES, variableString);
         VariablesPlugin.getDefault().savePluginPreferences();
         fInternalChange = false;
     }

     /**
      * Fire a change notification for the given variable.
      *
      * @param variable the variable that has changed
      */
     protected void notifyChanged(IValueVariable variable) {
         if (!fInternalChange) {
             IValueVariable existing = getValueVariable(variable.getName());
             if (variable.equals(existing)) {
                 // do not do change notification for unregistered variables
 getNotifier().notify(new IValueVariable[]{variable}, CHANGED);
             }
         }
     }

     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#generateVariableExpression(java.lang.String, java.lang.String)
      */
     public String generateVariableExpression(String varName, String arg) {
         StringBuffer buffer = new StringBuffer ();
         buffer.append("${"); //$NON-NLS-1$
 buffer.append(varName);
         if (arg != null) {
             buffer.append(":"); //$NON-NLS-1$
 buffer.append(arg);
         }
         buffer.append("}"); //$NON-NLS-1$
 return buffer.toString();
     }
     
     /* (non-Javadoc)
      * @see org.eclipse.debug.internal.core.stringsubstitution.IStringVariableManager#performStringSubstitution(java.lang.String, boolean)
      */
     public String performStringSubstitution(String expression, boolean reportUndefinedVariables) throws CoreException {
         return new StringSubstitutionEngine().performStringSubstitution(expression, reportUndefinedVariables, true, this);
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.variables.IStringVariableManager#validateStringVariables(java.lang.String)
      */
     public void validateStringVariables(String expression) throws CoreException {
         new StringSubstitutionEngine().validateStringVariables(expression, this);
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.variables.IStringVariableManager#getContributingPluginId(org.eclipse.core.variables.IStringVariable)
      */
     public String getContributingPluginId(IStringVariable variable) {
         if (variable instanceof StringVariable) {
             return ((StringVariable) variable).getConfigurationElement().getContributor().getName();
         }
         return null;
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.Preferences.IPropertyChangeListener#propertyChange(org.eclipse.core.runtime.Preferences.PropertyChangeEvent)
      */
     public void propertyChange(PropertyChangeEvent event) {
         if (PREF_VALUE_VARIABLES.equals(event.getProperty())) {
             synchronized (this) {
                 if (!fInternalChange) {
                     fValueVariables.clear();
                     loadPersistedValueVariables();
                     loadContributedValueVariables();
                 }
             }
         }
     }
 }

